Repository navigation
fix(lint)!: the object_extension publish door judges a computed field an extension adds by the build's field-formula pass, against the extended object as served - #33119
Conversation
…eproduction) Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
…d's formula as the build does Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
…'s formula judgment Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
📓 Docs Drift CheckThis PR changes 3 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 4 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 173 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8889d9021bc5f416c9d64f231d0c51f49019bd6e && git checkout 8889d9021bc5f416c9d64f231d0c51f49019bd6e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin f5121e84d56d4e92a4cc40171c65dab84d801b89 bb8a46225eb8eef07f93863a1adba76b3976e0e1 && git checkout -B drift-repro f5121e84d56d4e92a4cc40171c65dab84d801b89 && git merge --no-ff bb8a46225eb8eef07f93863a1adba76b3976e0e1
node scripts/docs-audit/affected-docs.mjs --json f5121e84d56d4e92a4cc40171c65dab84d801b89
|
|
domain:spec#2 · os-tesla · session_01L9qibvfX5bawZKtfa6u7j9 Contract reviewServed-tier: Check-runs at review time (2026-11-13T16:27Z): 67 runs, 56 success, 11 skipped, 0 failed, 0 pending. Local runs at head, in the scratch tree: ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentReusing the build's walk behind a write-type fence is the right shape: one judge, the registry entry declares the type, the gate owns no judgment. What is not yet right is the universe: an extension's stored self lives folded inside No must-fix. Two should-fix:
Nits:
S1 and S2 are owed before landing; this reviewer delta-reviews the fix head. Implemented-by: VERDICT: PASS |
…he package's stored extension The runtime authoring gate gathered the extension's target as served before the write, so the package's stored extension (which the write replaces) still lent its fields to the judgment. Dropping a field the extension's own formula reads was admitted, and re-targeting the extension's own lookup was judged over the old target. The gate now takes the target from the registry fold that leaves the written package's `extend` contributions out (`withoutExtensionsOf`, applied to the target by name only), and passes the same narrowing through the stored-row fold for a target with its own stored object row. Every other caller of both folds reaches the call it made before. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
…tadata, and its location parity stated exactly The CEL row's runtime column gains the object_extension marker and the door paragraph names the formula pass. The changeset and semantic entry say that location parity with the build is index-exact for a package with one extension entry, state the republish semantics, and drop the weaker `os validate` acceptance alternative. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
…ular type The registry list narrowed the target by comparing the listed type with both spellings, which brought back an executable `'objects'` literal the #8862 census forbids. The narrowing is now decided once from the collection's singular type, so the plural retry narrows the same way. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
…sion-write fold pins Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
|
domain:spec#2 · os-tesla · session_01L9qibvfX5bawZKtfa6u7j9 Contract reviewServed-tier: Check-runs at review time (2026-11-13T21:03Z): 68 runs, 58 success, 10 skipped, 0 failed, 0 pending. The PR is a draft, Local runs at head, in the scratch tree: ① Derived judgments
② Semver levelUnchanged and still correct: ③ Boundary flags
④ Design judgmentThe fix is the right shape. It composes two answers of the registry's own fold by name, adds no fold of its own, and makes the candidate the target as it will be served after the write. Passing the option through the stored fold is needed, as the stored-row ablation shows. No must-fix. S1 is closed. S2 is closed in the tree; its PR-body line waits on the seat. One should-fix, new at this head's review:
Nits:
S3 is owed before landing. If it is resolved by prose and a card, this record continues under the prose-only head-move rule; a code fix owes a delta review. Implemented-by: VERDICT: PASS |
… optional package arguments The changeset, the semantic entry and the docblocks now say that a package-wide draft publish whose drafts depend on a sibling extension draft is refused (#33152), and that publishing the extension first through the per-item door works. The changeset names the metadata-protocol package, which carries the host half. The two folds' docblocks describe their optional arguments, and the extender fold hands both package options to the registry when both are given rather than dropping one; no caller passes both today. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
…-1oe8qz # Conflicts: # packages/lint/src/validate-expressions.ts
…r id The printed migration guidance of a semantic entry carries no tracker number. The batch-publish sentence keeps its substance; the changeset and the docblocks still cite the follow-up card. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9
|
domain:spec#2 · os-tesla · session_01L9qibvfX5bawZKtfa6u7j9 维护者速读
席位意见:可以落地。 席位核对ACCEPT · seat
|
…-loearv Second and last merge of main for #32607, at the seat's landing request (the head was 25 commits behind). Clean: protocol.ts auto-merged (#33119's hunks are elsewhere in the file). Co-authored-by: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016WV4czBmkW6UeWPazkWq7L
Requested by Tesla · project thread
Fixes #33072
Clause-②: no (narrowing) — the runtime
object_extensionpublish door (Studio's add-field on a.custompair,PUT /api/v1/meta/object_extension/TARGET, the MCP and AI authoring doors) now refuses an extension whose computed field carries anexpressionos buildrefuses. No exported type or signature changes. Measured first-party exposure: 0 newly refused items.Metadata-protocol: narrowed
Skills-docs: none owed — this PR edits
content/docs/deployment/validating-metadata.mdx(the CEL row's runtime column gainsᵒˣ, and the object extension door paragraph names the formula pass);skills/objectstack-datateachesdefineObjectExtension({ extend, fields })and the computed-field rules without saying the runtime door is exempt, so the door now gives the verdict the build already gives.维护者速读
.custom对上加字段、PUT /api/v1/meta/object_extension/对象、MCP/AI 作者)时,现在会对扩展新增的计算字段跑和os build相同的公式检查(同一条规则、同一份遍历,不另写一套)。公式调用不存在的函数、读对象上没有的字段,发布返回 422INVALID_METADATA,规则 id 与 build 相同,位置为object '对象' · objectExtensions[0] · field '字段' expression。重新发布时,按「去掉本包已存扩展后的目标对象」来判(写入会替换它),与 build 看到的一致。publishPackageDrafts)时,若草稿依赖同批的另一个扩展草稿会被拒(publishPackageDrafts judges a package's drafts without its own extension drafts, so a formula or hop into a field a sibling extension draft adds is refused and the whole batch rolls back #33152),先用单项发布口发布该扩展即可。回滚即 revert。packages/lint/src/validate-expressions.ts(extensionWrite)、packages/lint/src/authoring-rules.ts(runtimeTypes加object_extension)、packages/metadata-protocol/src/protocol-authoring-gate.ts(objectExtensionWriteFold/withTargetFoldedWithout)、packages/runtime/src/domains/meta-object-extension-item.test.ts(真实保存+发布口的 lint: the object_extension publish door runs no field-formula judgment, so a computed field os build refuses still publishes through PUT /meta/object_extension #33072 一节,6 条)。pnpm --filter @objectstack/runtime exec vitest run src/domains/meta-object-extension-item.test.ts -t "33072"(需先构建 metadata-protocol),预期 6 条全过:坏公式被拒、合法公式发布、重新发布时去掉被引用字段被拒、改指查找字段后按新目标判、目标有已存对象行时同样被拒。What changes
Lint: the door runs the build's one pass.
packages/lint/src/authoring-rules.ts.validateStackExpressionsdeclaresobject_extensioninruntimeTypes, soruntimeAuthoringRulesFor('object_extension')answersvalidateStackExpressions,validateSecurityRoleWord(registry order).packages/lint/src/validate-expressions.ts. On anobject_extensionwrite (runtimeWriteType),runStackExpressionPassesruns exactly one pass, by construction: lint: a stored computed field added by an object extension is never judged by the field-formula pass (stored-expression warnings and expression errors skip it) #32875's extension walk (judgeFieldFormulaover each field of eachobjectExtensionsByTargetentry). The object walk and the flows walk read an empty list under it, and the function returns right after the extension walk. No second field walk and no second fold: the door's snapshot is{ objects: LIVE_OBJECTS, permissions, books, datasets, objectExtensions: [WRITTEN_ENTRY] }, and the walk reads the target through the indexes already built overobjectsAsServed.packages/lint/src/runtime-gate.ts. Comment only, on theobject_extensionrow ofTYPE_TO_STACK_KEY.Metadata-protocol: the target is the one the write will serve.
A package holds one extension per target, so a republish replaces the stored one. The gate used to hand the walk the target as served before the write, with the stored extension still folded in. That was wrong both ways: dropping a field the extension's own formula reads was admitted, and a formula over a lookup the same write re-targets was judged over the old target.
packages/metadata-protocol/src/protocol-authoring-gate.ts.objectExtensionWriteFoldnames the narrowing for anobject_extensionwrite that names a package: the registry's existingwithoutExtensionsOffold option, on the body'sextendtarget.withTargetFoldedWithouttakes only the target's entry fromlistItems('object', undefined, { withoutExtensionsOf })and keeps every other object as served, because the option is package-wide and the package's extensions of other objects are not being written. The narrowing is keyed on the singular type, so the plural retry narrows the same way.protocol-stored-collection-fold.ts,protocol-object-extender-fold.ts, and the two private wrappers inprotocol.ts(amendment 6858898742). There is a new optional trailing argument on each fold:targetFoldonfoldStoredCollection, andwithoutExtensionsOfonfoldObjectExtendersFromRegistry. They apply the same narrowing to a target that has its own storedobjectrow, and only to that row. Every other caller passes nothing and reaches the call it made before. With bothonlyExtensionsOfandwithoutExtensionsOfgiven, both are handed to the registry, which leaves out a contribution either one leaves out; no caller passes both today. Both method docblocks state this.Docs, migration and changeset.
content/docs/deployment/validating-metadata.mdx. The CEL row's runtime column is now✓ᶠᵃᶜʰᵒˣᵒᵛᵖ:ᵒˣgoes beforeᵒ, because afterᵖthe longest-first reading would takeᵖᵒ. One sentence was added to the object extension door paragraph.packages/spec/src/migrations/entries/semantic/18.object-extension-door-computed-field-expression-judged.tsis the ADR-0087 D3 entry the narrowing owes. There is no D2 conversion, because how a refused expression is repaired is the author's fact..changeset/33072-object-extension-door-formula.md:@objectstack/lintminor,@objectstack/specminor,@objectstack/metadata-protocolpatch, both declaration lines, BREAKING banner, migration.Known limit (#33152). A package-wide draft publish (
publishPackageDrafts) whose drafts depend on a sibling extension draft is refused, because that batch's pending set carries no extension drafts. Publishing the extension first through the per-item door works. The same gap exists onmainfor object drafts. The changeset, the entry and the[#33072]docblocks state it.Tests
packages/lint/src/runtime-gate.object-formula-writes.test.ts, new describe#33072(10 cases) at the real gate (runRuntimeAuthoringRules). Cases:sqrt(record.amount)is refused underexpression-invalidat the build's location;packages/runtime/src/domains/meta-object-extension-item.test.ts, new describe[#33072](6 cases) through the realsaveMetaItem+publishMetaItemover better-sqlite3 and the realObjectQL/SchemaRegistry. Cases:sqrt(record.name)→ 422expression-invalidatobject 'crm_account' · objectExtensions[0] · field 'score__c' expression, the field not served and the row left a draft;bonus__cwhiletotal__creads it → 422 (was 200);ref__cfromcrm_regiontocrm_accountwithvia__c: record.ref__c.name→ 200 (was 422 againstcrm_region). This is also the first hop pinned on the real door;objectrow (the pair's runtime-authoredcrm_custom_lead) → 422.packages/metadata-protocol/src/protocol-authoring-gate.extension-write-fold.test.ts(new, 7 cases):objectExtensionWriteFold: which writes narrow;withTargetFoldedWithout: only the target is replaced;foldObjectExtendersFromRegistry: what each package argument hands the registry, both included.packages/lint/src/validate-security-posture.runtime-surface.test.ts: the security-role-word lint skips fields declared through objectExtensions, so a field named role passes validate, build and verify #31998 extension case's roster andrulesRunpins now read['validateStackExpressions', 'validateSecurityRoleWord'].Reproduction at
main2c98485d, with the pins committed first: 8 of the 10 lint door cases were red. The door publishedsqrt(record.amount)with 0 errors while the build refused it.Ablations (fix committed first, mutated through scripts/ablation-replace.mjs, each restore read blob == HEAD and git diff HEAD empty)
object_extensiondropped from the rule'sruntimeTypes, lint rebuilt, dist marker proved absentextensionWriteforcedundefined), metadata-protocol rebuiltfoldStoredCollectionbroken), metadata-protocol rebuiltAcceptance notes
2c98485d:examples/app-showcasehas one extension entry with 3 plain fields;packages/plugins/plugin-authhas one with asecretfield; HotCRM (4f0ffd8, re-verified at0009f58) has none. 0 carry anexpression, so the door refuses nothing first-party.objectExtensions[0]. The build of a package with several entries prints that entry's own index, andpackages[j].manifest.objectExtensions[i]in an artifact stack. Rule id, object, field and message are the same in every case.object_extensiondraft's_diagnosticsnow carries the formula verdict, becausedraftPublishVerdictreads the same roster. No prose face names it, and no pin covers it.label()or for the draft_diagnostics. The lint "(the differential)" case passes by the object-walk fence.objectExtensionItemRefusalstep 5), so at the door the formula pass never sees it. Only the lint API reaches the "judged on what does not depend on the target's fields" arm.@objectstack/metadata-protocol(patch), which carries the host half.validationsjudged at neither door, although the registry registers them on the object.ruling|Ruling-ref|maintainer|⛔) over the surface: the readings that bear on this diff are "the per-write snapshot is not widened for cross-item judgments" (rulings5791822697item 3 and6104584601) and "⛔ no context row for symmetry" (runtime-gate.ts). This diff adds no context collection and no snapshot key; it narrows the existingobjectslist for the target. None is reversed.packages/lint, inpackages/metadata-protocol(the gate plus the two folds' optional pass-through, amendment 6858898742), in the docs page and in the semantic entry.packages/runtimeis test only.Verification
Readings at HEAD
f57cc273unless noted.f57cc273is the r2 prose and metadata commit on top ofac6dd79d, which mergedorigin/mainbf02d33bcleanly. main is ahead again and merges clean; it was not merged again.Tests
Each heavy command ran under
scripts/pm/os-verify-lock.sh(VERDICT command-exit 0). Exit codes were captured before any pipe.f57cc273, afterpnpm build --concurrency=2(exit 0):@objectstack/metadata-protocolfull suite: Test Files 338 passed | 7 skipped (345), Tests 22810 passed | 36 skipped;@objectstack/specfull suite: Test Files 931 passed (931), Tests 28389 passed | 1 todo;meta-object-extension-item.test.ts: 29 passed (29), the 6[#33072]cases among them.ac6dd79d(the r1 head; r2 changes comments, the changeset, the entry's text, one fold's option hand-off for a case no caller reaches, and test files only):pnpm check:dual-build-cjs-loadsexit 0;(narrowing)arm): 460 files and 3874 tests passed, 42 skipped;@objectstack/runtimefull suite: 11938 passed, 19 skipped;@objectstack/lintfull suite: 8751 passed.ac6dd79d: 68 check runs, every one success or skipped.Gates
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackatf57cc273derives 119 families. Each ran with stdin from/dev/null: all 119 exited 0. The--ranreconciliation read:119 derived famil(ies) accounted for — 119 run, 0 NOT-MEASURED.🤖 Generated with Claude Code
https://claude.ai/code/session_01L9qibwwiSN1TXWjohzCZN9